Skip to main content

第06章 多线程

第6章 多线程

一、定义

  • 进程:有独立的内存空间
  • 线程:线程有时被称为轻量级进程。进程和线程都提供了一个执行环境,但创建新线程所需的资源比创建新进程少。线程存在于一个进程中-每个进程至少有一个线程。线程共享进程的资源,包括内存和打开的文件。这有助于通讯高效但可能存在潜在的问题。

二、创建线程

  • 方法一:手动创建一个线程,这个线程执行完就结束了。(人为的管理、控制线程)
    • 一种方法是继承Thread类
    • 一种方法是实现Runnable(推荐使用和这个,因为java里面只能继承一个类,但是实现可以实现多个类,所以珍贵的资源继承应该让出去,这也是官方推荐的方法)
  • 方法二:通过线程池,如果有需要获取线程就在池子里面找一个,用完之后收回,还是放在在线程池里面。节约资源,因为创建线程的时候需要消耗资源。(不需要考虑线程的管理,线程是新创建的还是已有的使用者不需要知道,由线程池管理)

三、多线程问题

  • 线程干涉:(两个线程都在写)A线程写入之后,B线程再次写入,然后导致A写的丢失了,这就是线程干涉
  • 内存不一致:(一个线程在写,一个线程在读)当不同线程对应该是相同数据的看到的不一致时,就会发生内存一致性错误。避免内存一致性错误的关键是理解先发生后发生的关系。这种关系只是保证一个特定语句的内存写入对另一个特定的语句可见。(总之就是不能线程交替读和写,要目专门读,要目专门写)

四、解决办法

  • 解决方法:Synchronized 同步修饰函数名(假设有一个线程在调用第一个方法,其他所有的线程都不能调用下面类里面的三个方法!注意是三个函数只要有一个在被调用,剩下的包括自己就都不允许其他的线程调用)【原理:这个对象有一个内部的锁,而执行的前提是要获取这个锁】【问题:静态方法会不会出问题?不会,因为每一个静态方法和类关联而不是一个对象,其实类还和一个class Object相关联,所以即使是静态方法也适用】

    public class SynchronizedCounter {
    private int c = 0;
    public synchronized void increment() { c++; }
    public synchronized void decrement() { c--; }
    public synchronized int value() { return c; }
    }
  • 当然也可以把粒度放小,

    public void addName(String name) {
    synchronized(this) {
    lastName = name;
    nameCount++;
    }
    nameList.add(name);
    }
  • 原子变量(加一个volatile修饰就可以了):前提是这个变量很容易出现内存一致性错误的问题,就需要加一个原子变量,这样编译器就会识别,保证这个变量的线程安全。但是不要把所有的变量都变成原子变量,这样会降低性能,矫枉过正。

  • 死锁:A线程等待B,B等待A,这样就会陷入死锁的循环。

  • 饿死:A想要访问一个共享资源,但是一直获取不到,就像被饿死了一样。(池化的方式,需要消除那些贪婪的线程,避免一个线程c长期贪婪的占用连接,)

  • 活锁:假设有AB两个线程,A产生数据发给B,如果A产生的数据特别多,导致B无法处理,传来的数据太多了导致B忙不来,也就是A得不到响应或者回复答复。

五、不可变对象

  • 推荐使用不可变对象,定义是:如果对象在构造后其状态不能改变,则该对象被认为是不可变的。最大程度地依赖不可变对象被广泛接受为创建简单、可靠代码的可靠策略。不可变对象在并发应用程序中特别有用。因为它们不能改变状态,所以它们不能被线程干扰破坏或在不一致的状态下观察到。
  • 不可变对象的话那我想要修改怎么办?创建一个新对象
  • 为什么需要不可变对象:线程干涉是因为(两个线程都在写W/W),内存不一致是因为一个在写一个在读(W/R),如果我们禁止掉写操作,就避免了这个问题。
  • 缺点:创建新对象的代价很大,但是其实Java的JVM让这个代价不是很大

为什么要选择线程安全的集合类型来维护客户端 Session?

如果有⼤量的⽤户(或者说客户端进⼊到Websocket连接),那么这个时候要做的动作就是把Session放⼊⼀个集合中【我⽤的集合是ConcurrentHashMap】,也就是在往集合⾥⾯写⼊内容,如果不能保证线程安全,由于在往集合中写⼊内容这⼀个操作不是原⼦操作,甚⾄涉及到很多复杂的过程,也就是说这些线程直接会相互⼲扰影响(我们可以举⼀个最简单的例⼦,哪怕i++这⼀⾏代码从汇编来看都涉及到三个操作,从内存拷⻉、⾃加、然后复制回去内存),最终导致的结果就是:可能同时有100个⽤户涌⼊进来,但是由于线程之间的⼲扰,最终能够维持的Session就只有80或者90多个,会有遗漏。为此,我们需要使⽤线程安全的集合来维护Sessions集合。

六、线程池

  • 执行器就是一个线程池,包含很多线程,线程池以池的方式进行管理
  • 使用工作线程可以最大限度地减少线程创建带来的开销。一种常见的线程池类型是固定数量线程池(没有创建线程的开销)。这种类型的池始终有指定数量的线程在运行。固定线程池的一个重要优点是,使用它的应用程序可以优雅地降级(我写的代码崩溃了但是不会影响线程池,独立的)。
  • 创建使用固定线程池的执行器的简单方法是调用java.util.concurrent.Executors中的newFixedThreadPool工厂方法此类还提供以下工厂方法:newCachedThreadPool方法创建具有可扩展线程池的执行器。此执行器适用于启动许多短期任务的应用程序。newSingleThreadExecutor方法创建一个执行器,一次执行一个任务。